使用 Rxjs 替代 Redux (三)
前言
在前面两篇文章中,我们已经实现了用 Rxjs 替代 Redux 的整个流程,上手开发业务也没有问题了。然而,这套方法有没有什么可以优化的地方呢?
重复的代码,DRY
随着项目的增大,你会慢慢发现这些代码的出现
// userList 的 loading 状态的流
export const userListLoading$ = new BehaviorSubject(false).debug('[user]:[userListLoading]');
// userList 的数据的流
export const userList$ = new BehaviorSubject([]).debug('[user]:[userList]');
// 加载 userList 的 action
export const loadUserList = params => {
  // loading
  userListLoading$.next(true);
  request({
    method: 'GET',
    url: '/user',
    data: params,
    sucCallback: (data) => {
      // 获取到数据,让 store 执行 next
      userList$.next(data);
    },
    errCallback: (err) => {
      // 错误处理
      showErrorToast(err);
    }
  })
};
接下来,如果有其他数据的增加,对应的代码很类似,只是流和 action 的命名不同而已
DRY 是 Don’t Repeat Yourself 的缩写。意思是说,在一个设计里,对于任何东西,都应该有且只有一个表示,其它的地方都应该引用这一处。这样需要改动的时候,只需调整这一处,所有的地方就都变更过来了。
可见,我们的设计其实有点违背 DRY 原则,重复的代码过多了!
实现 entity_generator
参照之前编写 redux generator 的经验,我们来实现一个 entity_generator,使用配置的方式生成我们需要的流和 action,从而精简代码,提高开发效率
普通数据类型
export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
- 变化的有:流的名字 data$、初始值、以及打印的前缀
 - example 属于 entity,其下包括多个数据流以及 action,data 属于其中一个数据流
 - 我们想实现的是对于每一个 entity,通过配置,生成其下的所有数据流和 action
 - 每一个配置是一个 json
 
具体的实现
enum type {
  DATA = 'data',
  VALUE_DATA = 'valueData',
  BOOL_DATA = 'boolData',
  ACTION = 'action',
  ASYNC_ACTION = 'asyncAction'
}
interface IConfig {
  type: type;
  name: string;
  init?: any;
  privateName?: boolean;
  handler?: (params?, entity?) => any;
  privateLoading?: boolean;
  actionLoading?: boolean;
  confirmLoading?: boolean;
  bindLoading?: boolean;
  requestOpts?: any;
  filter?: {
    init: any;
  };
  data?: {
    init: any;
    handler: (params?, response?, entity?) => any;
  }
}
type IConfigs = IConfig[];
// 暴露一个方法,传入 entityName 和配置数组,生成对应的 entity
export default (entityName: string, configs: IConfigs) => {
  // 用一个 result 来存储 entity
  const result: any = {};
  // 遍历配置,生成数据
  configs.forEach(config => {
    // 获取配置中的属性
    const {
      type,     // 类型
      name,     // 名称
      init      // 初始值
    } = config;
    // 根据 type 来生成数据
    switch (config.type) {
      case actionTypes.DATA:
        /** create data Observable */
        result[`${name}$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}]`);
        break;
    }
  });
  return result;
};
对应的配置
{
  type: actionTypes.DATA,
  name: 'data',
  init: 0
}
值类型
export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
export const setData = params => {
  data$.next(params);
};
- 变化的有:名称、初始值、以及生成的 action 名字
 - 我们约定设置该值的 action 名字规则为 setXXX
 - 我们需要一个转换驼峰式写法的函数如下
const toCamel = src => `${src[0].toUpperCase()}${src.substr(1, src.length)}`; 
具体的实现
  case actionTypes.VALUE_DATA:
    /** create data Observable */
    result[`${name}$`] = new BehaviorSubject(init || 0).debug(`[${entityName}]:[${name}]`);
    /** set data operation */
    result[`set${toCamel(name)}`] = params => {
      result[`${name}$`].next(params);
    };
    break;
对应的配置
{
  type: actionTypes.VALUE_DATA,
  name: 'data',
  init: 0
}
布尔类型
export const modalShow$ = new BehaviorSubject(false).debug('[example]:[modalShow]');
export const showModal = () => {
  modalShow$.next(true);
};
export const hideModal = () => {
  modalShow$.next(false);
};
- 变化的有:名称、初始值、以及生成的 action 名字
 - 我们约定设置该值的数据流名字为 xxxShow$,action 名字规则为 setXXX
 
具体的实现
  case actionTypes.BOOL_DATA:
    /** create data Observable. */
    result[`${name}Show$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}Show]`);
    /** show operation */
    result[`show${toCamel(name)}`] = () => {
      result[`${name}Show$`].next(true);
    };
    /** hide operation */
    result[`hide${toCamel(name)}`] = () => {
      result[`${name}Show$`].next(false);
    };
    break;
对应的配置
{
  type: actionTypes.BOOL_DATA,
  name: 'modal',
  init: false
}
action 类型
export const doSomething = params => data$.next('Change');
- 变化的有:action 名字以及对应的操作
 - 我们用一个 handler(params, result) 函数来实现操作
 
具体的实现
case actionTypes.ACTION:
    /** create operation */
    result[`${name}`] = params => {
      handler(params, result);
    };
    break;
对应的配置
{
  type: actionTypes.ACTION
  name: 'doSomething',
  handler: (params, entity) => entity.data$.next('Change');
}
异步 action 类型
// loading、data
export const listLoading$ = new BehaviorSubject(false).debug('[example]:[listLoading]');
export const listData$ = new BehaviorSubject([]).debug('[example]:[listData]');
// 过滤器
export const listFilterData$ = new BehaviorSubject({}).debug('[example]:[listFilterData]');
export const setListFilterData = data => listFilterData$.next(data);
// 分页
export const listCurPage$ = new BehaviorSubject(1).debug('[example]:[listCurPage]');
export const setListCurPage = data => listCurPage$.next(data);
export const listPageSize$ = new BehaviorSubject(20).debug('[example]:[listPageSize]');
export const setListPageSize = data => listPageSize$.next(data);
export const listTotal$ = new BehaviorSubject(0).debug('[example]:[listTotal]');
// 请求
export const list = params => {
  listLoading$.next(true);
  request({
    method: 'GET',
    url: '/list',
    data: params,
    sucCallback: (data) => {
      listLoading$.next(false);
      listData$.next(data.list);
      listTotal$.next(data.count);
    },
    errCallback: (err) => {
      listLoading$.next(false);
      showErrorToast(err);
    }
  })
};
只要做一下约定,就可以让以上数据标准化。
具体的实现
case actionTypes.ASYNC_ACTION:
    /** private loading,auto emit data in async operation */
    if (privateLoading) {
      result[`${name}Loading$`] = new BehaviorSubject(false).debug(`[${entityName}]:[${name}Loading]`);
    }
    /** create data Observable,auto emit data when request success */
    if (data) {
      result[`${name}Data$`] = new BehaviorSubject(data.init).debug(`[${entityName}]:[${name}Data]`);
    }
    /** create filter Observable */
    if (filter) {
      result[`${name}FilterData$`] = new BehaviorSubject(filter.init).debug(`[${entityName}]:[${name}FilterData]`);
      result[`set${toCamel(name)}FilterData`] = data => {
        result[`${name}FilterData$`].next(data);
      };
    }
    if (pagination) {
      result[`${name}CurPage$`] = new BehaviorSubject(1).debug(`[${entityName}]:[${name}CurPage]`);
      result[`set${toCamel(name)}CurPage`] = data => {
        result[`${name}CurPage$`].next(data);
      };
      result[`${name}PageSize$`] =
        new BehaviorSubject(pagination.pageSize || 20).debug(`[${entityName}]:[${name}PageSize]`);
      result[`set${toCamel(name)}PageSize`] = data => {
        result[`${name}PageSize$`].next(data);
      };
      result[`${name}Total$`] = new BehaviorSubject(0).debug(`[${entityName}]:[${name}Total]`);
    }
    /** create the main operation */
    result[name] = (params?) => {
      /** loading => true */
      privateLoading && result[`${name}Loading$`].next(true);
      request({
        method: requestOpts.method,
        url: requestOpts.url(params, result),
        /** requestOpts.data is a function that return data according to params and the entity */
        data: requestOpts.data && requestOpts.data(omit(['sucCallback', 'errCallback'], params), result),
        sucCallback: response => {
          /** loading => false */
          privateLoading && result[`${name}Loading$`].next(false);
          /** set data according to response */
          data && result[`${name}Data$`].next(data.handler(params, response, result));
          /** set total according to response if pagination */
          pagination && result[`${name}Total$`].next(pagination.total(params, response, result));
          /** do callback if needed */
          requestOpts.sucCallback && requestOpts.sucCallback(params, response, result);
        },
        errCallback: err => {
          /** loading => false */
          privateLoading && result[`${name}Loading$`].next(false);
          /** do callback if needed */
          requestOpts.errCallback && requestOpts.errCallback(params, err, result);
        }
      });
    };
    break;
对应的配置
  {
    type: actionTypes.ASYNC_ACTION,
    name: 'list',
    privateLoading: true,
    requestOpts: {
      method: 'GET',
      url: (params, entity) => `/list`,
      data: (params, entity) => Object.assign({},
        entity[`listFilterData$`].getValue(), {
          limit: entity[`listPageSize$`].getValue(),
          offset: (entity[`listCurPage$`].getValue() - 1) * entity[`listPageSize$`].getValue()
        }, params)
    },
    filter: {
      init: {}
    },
    pagination: {
      total: (params, response, entity) => response.count
    },
    data: {
      init: [],
      handler: (params, response, entity) => response.list
    }
  }
总结
通过实现以上的 entity_generator,在开发过程中不仅节省了代码,也提高了开发效率,约定了标准的命名规范,最终提升了协同的效率。
记住,Don’t repeat yourselef.